package org.airsonic.player.security;

import org.airsonic.player.controller.SubsonicRESTController.APIException;
import org.airsonic.player.controller.SubsonicRESTController.ErrorCode;
import org.airsonic.player.domain.User;
import org.airsonic.player.domain.User.Role;
import org.airsonic.player.service.SecurityService;
import org.airsonic.player.service.SettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.ldap.authentication.LdapAuthenticator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;

import java.util.Collection;
import java.util.Set;

public class CustomLDAPAuthenticatorPostProcessor implements ObjectPostProcessor<LdapAuthenticator> {
    private static final Logger LOG = LoggerFactory.getLogger(CustomLDAPAuthenticatorPostProcessor.class);

    private final SecurityService securityService;
    private final SettingsService settingsService;

    public CustomLDAPAuthenticatorPostProcessor(SecurityService securityService, SettingsService settingsService) {
        this.securityService = securityService;
        this.settingsService = settingsService;
    }

    @Override
    public <O extends LdapAuthenticator> O postProcess(O object) {
        return (O) new CustomLDAPAuthenticator(object, securityService, settingsService);
    }

    public static class CustomLDAPAuthenticator implements LdapAuthenticator {
        public static final String AIRSONIC_USER_ATTRIBUTE = "org.airsonic.player.user";
        private final LdapAuthenticator delegate;
        private final SecurityService securityService;
        private final SettingsService settingsService;

        public CustomLDAPAuthenticator(LdapAuthenticator delegate, SecurityService securityService, SettingsService settingsService) {
            this.delegate = delegate;
            this.securityService = securityService;
            this.settingsService = settingsService;
        }

        @Override
        public DirContextOperations authenticate(Authentication authentication) {
            // shouldn't really happen because the provider wouldn't be configured in GlobalSecurityConfig
            if (!settingsService.isLdapEnabled()) {
                throw new BadCredentialsException("LDAP authentication disabled.");
            }

            if (authentication instanceof UsernameSaltedTokenAuthenticationToken) {
                LOG.debug("LDAP Authentication attempt cannot be done via hashed password (salted tokens)");
                throw new BadCredentialsException("LDAP authentication cannot be done via hashed passwords.",
                        new APIException(ErrorCode.NOT_AUTHENTICATED_UPGRADE_TO_NON_HASHED));
            }

            // User must be defined in Airsonic, unless auto-shadowing is enabled.
            String username = authentication.getName();
            User user = securityService.getUserByName(username, false);
            if (user == null && !settingsService.isLdapAutoShadowing()) {
                throw new BadCredentialsException("User does not exist.");
            }

            // LDAP authentication must be enabled for the given user.
            if (user != null && !user.isLdapAuthenticated()) {
                throw new BadCredentialsException("LDAP authentication disabled for user.");
            }

            DirContextOperations dco = delegate.authenticate(authentication);

            if (dco != null) {
                if (user == null) {
                    User newUser = new User(username, null, true, 0L, 0L, 0L, Set.of(Role.STREAM, Role.SETTINGS));
                    securityService.createUser(newUser, "", "Autogenerated for new LDAP user");
                    LOG.info("Created local user '{}' for DN {}", username, dco.getDn());
                    user = securityService.getUserByName(username, false);
                }

                dco.addAttributeValue(AIRSONIC_USER_ATTRIBUTE, user);
            }

            return dco;
        }

    }

    public static class CustomLDAPAuthoritiesPopulator implements LdapAuthoritiesPopulator {
        @Override
        public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
            User user = (User) userData.getObjectAttribute(CustomLDAPAuthenticator.AIRSONIC_USER_ATTRIBUTE);
            if (user == null) {
                return AuthorityUtils.NO_AUTHORITIES;
            }
            return SecurityService.getGrantedAuthorities(user);
        }

    }
}
